using SharpGraphT.Event;
using SharpGraphT.Sharp.Sharp.Util;
using SharpGraphT.Sharp.System.Collections.Generic;

namespace SharpGraphT.Graph;

[Serializable]
public class AsSubgraph<TV, TE> : AbstractGraph<TV, TE>
    where TE : class, new() {
    public static readonly long SerialVersionUid = -1471811754881775298L;

    private static readonly string NoSuchEdgeInBase =
        "no such edge in base graph";

    private static readonly string NoSuchVertexInBase =
        "no such vertex in base graph";

    private static readonly string CannotCreateNewVerticesFromSubgraph =
        "Cannot create new vertices from subgraph";

    protected readonly HashSet<TE> edgeSet = new HashSet<TE>();
    protected readonly HashSet<TV> vertexSet = new HashSet<TV>();
    protected readonly IGraph<TV, TE> @base;
    protected readonly IGraphType baseType;
    protected readonly bool isInduced;

    public AsSubgraph(IGraph<TV, TE> @base, ISet<TV> vertexSubset = null,
        ISet<TE> edgeSubset = null) {
        this.@base = @base;
        baseType = @base.Type;
        isInduced = edgeSubset == null;

        if (@base is IListenableGraph<TV, TE> listenableGraph) {
            new BaseGraphListener(this, listenableGraph);
        }

        Initialize(vertexSubset, edgeSubset);
    }

    public override IReadOnlySet<TE> GetAllEdges(TV sourceVertex,
        TV targetVertex) {
        if (ContainsVertex(sourceVertex) && ContainsVertex(targetVertex)) {
            return @base.GetAllEdges(sourceVertex, targetVertex)
                .Where(edgeSet.Contains).ToHashSet();
        }

        return null;
    }

    public override TE GetEdge(TV sourceVertex, TV targetVertex) =>
        GetAllEdges(sourceVertex, targetVertex)?.FirstOrDefault();

    public override Func<TV> VertexSupplier => @base.VertexSupplier;
    public override Func<TE> EdgeSupplier => @base.EdgeSupplier;

    public override TE AddEdge(TV sourceVertex, TV targetVertex) {
        AssertVertexExist(sourceVertex);
        AssertVertexExist(targetVertex);

        if (!@base.ContainsEdge(sourceVertex, targetVertex)) {
            throw new ArgumentException(NoSuchEdgeInBase);
        }

        var edges = @base.GetAllEdges(sourceVertex, targetVertex);

        foreach (var e in edges) {
            if (ContainsEdge(e)) {
                continue;
            }

            edgeSet.Add(e);
            return e;
        }

        return null;
    }

    public override bool AddEdge(TV sourceVertex, TV targetVertex, TE e) {
        if (e == null) {
            throw new NullReferenceException();
        }

        if (!@base.ContainsEdge(e)) {
            throw new ArgumentException(NoSuchEdgeInBase);
        }

        AssertVertexExist(sourceVertex);
        AssertVertexExist(targetVertex);

        Objects.Assert(Objects.Equals(@base.GetEdgeSource(e), sourceVertex));
        Objects.Assert(Objects.Equals(@base.GetEdgeTarget(e), targetVertex));

        return edgeSet.Add(e);
    }

    public override TV AddVertex() =>
        throw new InvalidOperationException(
            CannotCreateNewVerticesFromSubgraph);

    public override bool AddVertex(TV v) {
        Objects.RequireNonDefault(v);

        if (!@base.ContainsVertex(v)) {
            throw new ArgumentException(NoSuchVertexInBase);
        }

        return vertexSet.Add(v);
    }

    public override bool ContainsEdge(TE e) => edgeSet.Contains(e);

    public override bool ContainsVertex(TV v) => vertexSet.Contains(v);

    public override IReadOnlySet<TE> EdgeSet => edgeSet;

    public override IReadOnlySet<TE> EdgesOf(TV vertex) {
        AssertVertexExist(vertex);
        return @base.EdgesOf(vertex).Where(edgeSet.Contains).ToHashSet();
    }

    public override int DegreeOf(TV vertex) {
        AssertVertexExist(vertex);

        if (!baseType.IsUndirected) {
            return InDegreeOf(vertex) + OutDegreeOf(vertex);
        }

        var degree = 0;
        var it = @base.EdgesOf(vertex).Where(edgeSet.Contains).GetEnumerator();

        while (it.MoveNext()) {
            var e = it.Current;
            degree++;
            if (GetEdgeSource(e).Equals(GetEdgeTarget(e))) {
                degree++;
            }
        }

        return degree;
    }

    public override IReadOnlySet<TE> IncomingEdgesOf(TV vertex) {
        AssertVertexExist(vertex);
        return @base.IncomingEdgesOf(vertex).Where(edgeSet.Contains)
            .ToHashSet();
    }

    public override int InDegreeOf(TV vertex) =>
        baseType.IsUndirected
            ? DegreeOf(vertex)
            : IncomingEdgesOf(vertex).Count;

    public override IReadOnlySet<TE> OutgoingEdgesOf(TV vertex) {
        AssertVertexExist(vertex);
        return @base.OutgoingEdgesOf(vertex).Where(edgeSet.Contains)
            .ToHashSet();
    }

    public override int OutDegreeOf(TV vertex) =>
        baseType.IsUndirected
            ? DegreeOf(vertex)
            : OutgoingEdgesOf(vertex).Count;

    public override bool RemoveEdge(TE e) => edgeSet.Remove(e);

    public override TE RemoveEdge(TV sourceVertex, TV targetVertex) {
        var e = GetEdge(sourceVertex, targetVertex);
        return edgeSet.Remove(e) ? e : null;
    }

    public override bool RemoveVertex(TV v) {
        if (ContainsVertex(v) && @base.ContainsVertex(v)) {
            RemoveAllEdges(EdgesOf(v));
        }

        return vertexSet.Remove(v);
    }

    public override IReadOnlySet<TV> VertexSet => vertexSet;

    public override TV GetEdgeSource(TE e) => @base.GetEdgeSource(e);

    public override TV GetEdgeTarget(TE e) => @base.GetEdgeTarget(e);

    public override IGraphType Type => @base.Type;

    public override double GetEdgeWeight(TE e) => @base.GetEdgeWeight(e);

    public override void SetEdgeWeight(TE e, double weight) =>
        @base.SetEdgeWeight(e, weight);

    private void Initialize(ISet<TV> vertexFilter, ISet<TE> edgeFilter) {
        if (vertexFilter == null && edgeFilter == null) {
            vertexSet.AddRange(@base.VertexSet);
            edgeSet.AddRange(@base.EdgeSet);
            return;
        }

        if (vertexFilter == null) {
            vertexSet.AddRange(@base.VertexSet);
        } else {
            if (vertexFilter.Count > @base.VertexSet.Count) {
                vertexSet.AddRange(
                    @base.VertexSet.Where(vertexFilter.Contains));
            } else {
                vertexSet.AddRange(vertexFilter.Where(v =>
                    !Objects.IsDefault(v) && @base.ContainsVertex(v)));
            }
        }

        if (edgeFilter == null) {
            edgeSet.AddRange(@base.EdgeSet.Where(e =>
                vertexSet.Contains(@base.GetEdgeSource(e)) &&
                vertexSet.Contains(@base.GetEdgeTarget(e))));
        } else {
            if (edgeFilter.Count > @base.EdgeSet.Count) {
                edgeSet.AddRange(@base.EdgeSet.Where(e =>
                    edgeFilter.Contains(e) &&
                    vertexSet.Contains(@base.GetEdgeSource(e)) &&
                    vertexSet.Contains(@base.GetEdgeTarget(e))));
            } else {
                edgeSet.AddRange(edgeFilter.Where(e =>
                    e != null && @base.ContainsEdge(e) &&
                    vertexSet.Contains(@base.GetEdgeSource(e)) &&
                    vertexSet.Contains(@base.GetEdgeTarget(e))));
            }
        }
    }

    [Serializable]
    private class BaseGraphListener {
        private static readonly long SerialVersionUid = 4343535244243546391L;

        private AsSubgraph<TV, TE> _graph;

        public BaseGraphListener(AsSubgraph<TV, TE> graph,
            IListenableGraph<TV, TE> listenableGraph) {
            _graph = graph;
            listenableGraph.EdgeAdded += GraphOnEdgeAdded;
            listenableGraph.EdgeRemoved += GraphOnEdgeRemoved;
            listenableGraph.VertexAdded += GraphOnVertexAdded;
            listenableGraph.VertexRemoved += GraphOnVertexRemoved;
        }

        private void GraphOnEdgeAdded(object? sender,
            GraphEdgeChangeEventArgs<TV, TE> e) {
            if (!_graph.isInduced) {
                return;
            }

            var edge = e.Edge;
            var source = e.EdgeSource;
            var target = e.EdgeTarget;
            if (_graph.ContainsVertex(source) &&
                _graph.ContainsVertex(target)) {
                _graph.AddEdge(source, target, edge);
            }
        }

        private void GraphOnEdgeRemoved(object? sender,
            GraphEdgeChangeEventArgs<TV, TE> e) {
            var edge = e.Edge;
            _graph.RemoveEdge(edge);
        }

        private void GraphOnVertexAdded(object? sender,
            GraphVertexChangeEventArgs<TV> e) { }

        private void GraphOnVertexRemoved(object? sender,
            GraphVertexChangeEventArgs<TV> e) {
            var vertex = e.Vertex;
            _graph.RemoveVertex(vertex);
        }
    }
}